// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.api

import android.content.Context
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.transitous.api.RoutingApi
import xyz.apiote.bimba.czwek.api.transitous.model.PedestrianProfile
import xyz.apiote.bimba.czwek.data.traffic.Place
import xyz.apiote.bimba.czwek.network.isNetworkAvailable
import xyz.apiote.bimba.czwek.repo.Colour
import xyz.apiote.bimba.czwek.repo.CongestionLevel
import xyz.apiote.bimba.czwek.repo.Event
import xyz.apiote.bimba.czwek.repo.Journey
import xyz.apiote.bimba.czwek.repo.JourneyParams
import xyz.apiote.bimba.czwek.repo.Leg
import xyz.apiote.bimba.czwek.repo.LineStub
import xyz.apiote.bimba.czwek.repo.LineType
import xyz.apiote.bimba.czwek.repo.OccupancyStatus
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.repo.TimeReference
import xyz.apiote.bimba.czwek.repo.TrafficResponseException
import xyz.apiote.bimba.czwek.repo.Vehicle
import xyz.apiote.bimba.czwek.units.Metre
import xyz.apiote.bimba.czwek.units.Mps
import xyz.apiote.bimba.czwek.units.Second
import java.time.Duration
import java.time.ZoneId
import java.time.ZonedDateTime

suspend fun getJourney(
	from: Place,
	to: Place,
	params: JourneyParams,
	context: Context
): List<Journey> {
	if (!isNetworkAvailable(context)) {
		throw TrafficResponseException(0, "", Error(0, R.string.error_offline, R.drawable.error_net))
	}

	return withContext(Dispatchers.IO) {
		val client = OkHttpClient.Builder()
			.addNetworkInterceptor { chain ->
				chain.proceed(
					chain
						.request()
						.newBuilder()
						.header("User-Agent", context.getString(R.string.applicationId))
						.build()
				)
			}
			.callTimeout(Duration.ofSeconds(60))
			.readTimeout(Duration.ofSeconds(60))
			.writeTimeout(Duration.ofSeconds(60))
			.connectTimeout(Duration.ofSeconds(60)).build()
		val response = RoutingApi(client = client).plan(
			from.getJourneyID(),
			to.getJourneyID(),
			maxTransfers = null,
			maxTravelTime = null,
			time = ZonedDateTime.of(params.getSafeDate(), params.getSafeTime(), ZoneId.systemDefault())
				.toOffsetDateTime(),
			arriveBy = params.timeReference == TimeReference.ARRIVE_BY,
			requireBikeTransport = params.bicycle,
			pedestrianProfile = if (params.wheelchairAccessible) PedestrianProfile.WHEELCHAIR else PedestrianProfile.FOOT,
		)
		val journeys = response.itineraries.map {
			val legs: List<Leg> = it.legs.map {
				Leg(
					Event(
						it.tripId ?: "",
						null,
						Time.fromOffsetTime(it.startTime, ZoneId.systemDefault()),
						0u,
						it.realTime,
						Vehicle(
							it.tripId ?: "",
							Position(0.0, 0.0),
							0u,
							Mps(0),
							LineStub(
								it.routeShortName ?: "",
								LineType.fromTransitous2(it.mode),
								Colour.fromHex(it.routeColor)
							),
							it.headsign ?: "",
							CongestionLevel.UNKNOWN,
							OccupancyStatus.UNKNOWN
						),
						boarding = 0xffu,
						alerts = emptyList(),
						exact = true,
						terminusArrival = false
					),
					Event(
						it.tripId ?: "",
						Time.fromOffsetTime(it.endTime, ZoneId.systemDefault()),
						null,
						0u,
						it.realTime,
						Vehicle(
							it.tripId ?: "",
							Position(0.0, 0.0),
							0u,
							Mps(0),
							LineStub(
								it.routeShortName ?: "",
								LineType.fromTransitous2(it.mode),
								Colour.fromHex(it.routeColor)
							),
							it.headsign ?: "",
							CongestionLevel.UNKNOWN,
							OccupancyStatus.UNKNOWN
						),
						boarding = 0xffu,
						alerts = emptyList(),
						exact = true,
						terminusArrival = false
					),
					Place.fromTransitousPlace(it.from),
					Place.fromTransitousPlace(it.to),
					it.agencyName,
					it.distance?.toDouble()?.let { Metre(it) },
					Second(it.duration),
					(it.intermediateStops ?: emptyList()).map { Stop(it) },
					decode(it.legGeometry.points)
					/*it.rental,
					it.steps,*/
				)
			}
			Journey(
				it.startTime.atZoneSameInstant(ZoneId.systemDefault()), it.endTime.atZoneSameInstant(
					ZoneId.systemDefault()
				), legs
			)
		}
		if (params.timeReference == TimeReference.ARRIVE_BY) {
			journeys.reversed()
		} else {
			journeys
		}
	}
}

/*
The following piece of code © Google, Apache License, Version 2.0
from https://github.com/googlemaps/android-maps-utils/blob/main/library/src/main/java/com/google/maps/android/PolyUtil.java
with changes to decode with precision 7
*/
fun decode(encodedPath: String): MutableList<Position> {
	val len = encodedPath.length

	val path: MutableList<Position> = ArrayList<Position>()
	var index = 0
	var lat = 0
	var lng = 0

	while (index < len) {
		var result = 1
		var shift = 0
		var b: Int
		do {
			b = encodedPath[index++].code - 63 - 1
			result += b shl shift
			shift += 5
		} while (b >= 0x1f)
		lat += if ((result and 1) != 0) (result shr 1).inv() else (result shr 1)

		result = 1
		shift = 0
		do {
			b = encodedPath[index++].code - 63 - 1
			result += b shl shift
			shift += 5
		} while (b >= 0x1f)
		lng += if ((result and 1) != 0) (result shr 1).inv() else (result shr 1)

		path.add(Position(lat * 1e-7, lng * 1e-7))
	}

	return path
}
